#define _POSIX_SOURCE

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

static int exit_internal_failure;
#define GETOPT_HELPER_EXIT_FAILURE exit_internal_failure
#include "../getopt_helper.h"
#include "../util.h"

#define NFORMAT 2
const char usage_str[] =
    "Usage: %s [ARG]...\n"
    "   or: %s OPTION\n"
    "Run COMMAND, ignoring hangup signals.\n"
    "\n"
    "      --help     display this help and exit\n"
    "      --version  output version information and exit"
    "\n"
    "If standard input is a terminal, redirect it from an unreadable file.\n"
    "If standard output is a terminal, append output to 'nohup.out' if "
    "possible,\n"
    "'$HOME/nohup.out' otherwise.\n"
    "If standard error is a terminal, redirect it to standard output.\n"
    "To save output to FILE, use 'nohup COMMAND > FILE'.\n"
    "\n"
    "NOTE: your shell may have its own version of nohup, which usually "
    "supersedes\n"
    "the version described here.  Please refer to your shell's documentation\n"
    "for details about the options it supports.\n"
    "\n"
    "Exit status:\n"
    "  125  if the nohup command itself fails\n"
    "  126  if COMMAND is found but cannot be invoked\n"
    "  127  if COMMAND cannot be found\n"
    "  -    the exit status of COMMAND otherwise\n";

static struct option const LONG_OPTIONS[] = { GETOPT_END_DECL };

#define DEV_NULL  "/dev/null"
#define FILE_NAME "nohup.out"

#define wclose(FD)                                                            \
	if (close((FD)) < 0) warn("close failed at %s:%d", __func__, __LINE__)
#define edup2(OLD, NEW)                                                       \
	if (dup2((OLD), (NEW)) != (NEW))                                      \
	err(exit_internal_failure, "dup2 failed at %s:%d", __func__, __LINE__)

enum {
	POSIX_NOHUP_FAILURE = 127,
};

enum IS_TTY {
	IS_TTY_FILE = 0,
	IS_TTY_TTY = !0,
	IS_TTY_CLOSED,
};
typedef struct IsTTY {
	enum IS_TTY in;
	enum IS_TTY out;
	enum IS_TTY err;
} __attribute__((aligned(16))) IsTTY;

static void reopen_stdin(void);
static int  get_out_fd(IsTTY is_tty);
static int  exec(int stderr_fd, char **argv);

static inline IsTTY
get_tty_status(void)
{
	return (IsTTY){
		/* do not care about closed for stdin and stderr */
		.in = isatty(STDIN_FILENO) ? IS_TTY_TTY : IS_TTY_FILE,
		.err = isatty(STDERR_FILENO) ? IS_TTY_TTY : IS_TTY_FILE,

		/* check if closed only for stdout */
		.out = isatty(STDOUT_FILENO)
			 ? IS_TTY_TTY
			 : (errno == EBADF ? IS_TTY_CLOSED : IS_TTY_FILE),
	};
}

int
main(int argc, char *argv[])
{
	exit_internal_failure =
	    (getenv("POSIXLY_CORRECT") ? POSIX_NOHUP_FAILURE : EXIT_CANCELED);

	int ch;
	while ((ch = getopt_long(argc, argv, "", LONG_OPTIONS, NULL)) != -1) {
		switch (ch) {
			GETOPT_END_CASE
		}
	}

	if (argc <= optind)
		errx(exit_internal_failure, "missing command to run");

	IsTTY is_tty = get_tty_status();

	if (!!is_tty.in) {
		reopen_stdin();
		warnx("ignoring input");
	}

	int out_fd = get_out_fd(is_tty);
	if (is_tty.out == IS_TTY_TTY) edup2(out_fd, STDOUT_FILENO);

	int saved_stderr_fd = STDERR_FILENO;
	if (!!is_tty.err) {
		saved_stderr_fd =
		    fcntl(STDERR_FILENO, F_DUPFD_CLOEXEC, STDERR_FILENO + 1);
		warnx("redirecting stderr to stdout");
		edup2(out_fd, STDERR_FILENO);
	}

	(void)close(out_fd);

	if (ferror(stderr)) return exit_internal_failure;

	(void)signal(SIGHUP, SIG_IGN);
	return exec(saved_stderr_fd, &argv[optind]);
}

static void
reopen_stdin(void)
{
	/* it is deliberately opened for *writing*,
	 * to ensure any read evokes an error */
	int null_fd = open(DEV_NULL, O_WRONLY | O_CLOEXEC);
	if (null_fd < 0)
		err(exit_internal_failure, "failed to open '%s'", DEV_NULL);

	edup2(null_fd, STDIN_FILENO);
	/* wclose(null_fd); */
	close(null_fd);
}

static int
get_out_fd(IsTTY is_tty)
{
	int out_fd = -1;

	if (is_tty.out == IS_TTY_TTY || !!is_tty.err) {
		int    flags = O_CREAT | O_WRONLY | O_APPEND | O_CLOEXEC;
		mode_t mode = S_IRUSR | S_IWUSR;
		mode_t old_umask = umask(~mode);

		char const *file = FILE_NAME;
		char	    out_file[PATH_MAX] = { 0 };

		out_fd = open("./" FILE_NAME, flags, mode);
		if (out_fd < 0) {
			warn("failed to open '%s'", "./" FILE_NAME);
			char const *home = getenv("HOME");
			if (!home) exit(exit_internal_failure);

			int printed = snprintf(out_file,
					       sizeof(out_file),
					       "%s/" FILE_NAME,
					       home);
			if (printed < 0 || printed >= (int)sizeof(out_file))
				errx(exit_internal_failure,
				     "path '${HOME}/%s' is too long",
				     FILE_NAME);

			file = out_file;
			out_fd = open(out_file, flags, mode);
			if (out_fd < 0)
				err(exit_internal_failure,
				    "failed to open '%s'",
				    out_file);
		}
		umask(old_umask);
		warnx("appending output to '%s'", file);
	}
	return out_fd;
}

static int
exec(int stderr_fd, char **argv)
{
	execvp(argv[0], argv);
	int exec_errno = errno;

	if (dup2(stderr_fd, STDERR_FILENO) == STDERR_FILENO) {
		errno = exec_errno;
		warn("failed to run command '%s'", argv[optind]);
	}

	return (exec_errno == ENOENT) ? EXIT_ENOENT : EXIT_CANNOT_INVOKE;
}
